Skip to content

Unicode和它的朋友们(上)

Author: [麟十一]

Link: [https://mp.weixin.qq.com/s/54cdPxMj2Lg37oLdeDj3xw]

写在前面

原本这一篇是要写“TTF里都有什么”,不过之前在解析TTF的时候我发现文件里所有的汉字都是用Unicode十六进制编码存储的,整个TTF文件从头到尾没有出现过汉字,所以我想先简单写一篇Unicode编码和汉字相转换的文章作为前言,这样下一篇就只需要专心解析TTF了

不过万万没想到,Unicode并没有我想的那么简单,它不是一个单独的个体单独解释Unicode总感觉解释不清楚,所以我最终把这一篇写成了字符编码史,这一篇又称“Unicode和它的100个周边”

1. 位,字节和字符

在计算机中,所有信息都是由1和0组成的二进制数字表示的,每一个0或1就是一个位(bit) ,位是计算机中最基本的单位,8个位构成一个字节(byte) 。而编码 就是一套电脑内的数字 (二/八/十/十六进制等)和字符相对应的规则 。字符通过编码转换成计算机可以识别的数字,这些数字通常使用字节来存储,将字节解码就会得到相应的字符或字符串。

字符串和字节的转换示意图

在Python中可以用encode()decode() 两个函数实现字符和编码的转换:

python
#1. 字符-->字节
char2bytes = '麟'.encode('utf-8')
print(char2bytes) # b'\xe9\xba\x9f'
print(type(char2bytes)) # <class 'bytes'>
#2. 字节-->字符
bytes2char = b'\xe9\xba\x9f'.decode('utf-8')
print(bytes2char) # '麟'
print(type(bytes2char)) # <class 'str'>

上面代码中b 代表后面的内容为字节类型\x 代表后面的数字为十六进制 ,可以看到汉字“麟”通过UTF-8编码之后变成了3个字节,字节解码之后又变成了字符串。 由于这篇文章涉及的概念很多,我做了一张图当作目录,这张图涵盖了本文的主要框架和内容,有些概念虽然没有在图中出现但在后文会提及。下图共分为三个部分,可以认为是计算机编码的三个发展阶段。先介绍第一阶段:ASCII

2. ASCII

第一阶段:ASCII

1967年,美国国家标准学会(American National Standard Institute , ANSI)制定了一套基于拉丁字母的电脑编码方案,称为美国信息交换标准代码(American Standard Code for Information Interchange, ASCII) 。最开始编码的字符很少,数字、字母加上不可见的字符一共才定义了128 个。由于计算机中1字节=8位,为了存储和计算方便,ASCII编码使用一字节中的低7位来存储128个编码,最高位设置为0 。ASCII中共有2^7=128个字符,其中包括33个控制字符(不可见)和95个可见字符,看一张ASCII编码示意表:

ASCII编码示意表

由上表可以看到,0-31和第127个字符属于控制字符(不可显示字符) ,其他都是可显示字符。其中48-57是0-9共10个阿拉伯数字,65-90为大写英文字母,97-122为小写英文字母 ,剩余字符是标点符号、运算符号等等。

最后一行被挡住的是“删除”

3. 扩展ASCII

第二阶段:本土化--扩展ASCII

由于标准ASCII只使用了字节中的低7位,1981年,IBM对ASCII进行了扩充,它使用了字节中的最高位,共扩充了128个字符,这就是扩展ASCII(Extended ACII) 。扩展ASCII中0-127个字符和ASCII保持一致,第128-255位为扩展字符,扩展字符主要包括框线、音标和欧洲非英语系的一些字母。

虽然扩展ASCII是由IBM制定的,但是在某种程度上来说,目前所有的非ASCII码,都是对标准ASCII码的扩充 ,所以我在图中用大括号把下面所有的内容都包括了进去(包括第三阶段的内容)。从扩展ASCII开始,编码进入了第二个阶段:本土化

4.ISO 8859

随着世界各国计算机技术的发展,标准ASCII和扩展ASCII无法满足各种语言的需要,因此各个国家开始制定自己的文字编码,下图中列出了其中4种:ISO 8859,BIG5,Shift_JIS和GB系列编码。

第二阶段:本土化--ISO 8859

先来介绍ISO 8859,ISO 8859也被称为ISO/IEC 8859 ,它是由国际标准化组织(International Organization for Standardization,ISO)和国际电工委员会(International Electrotechnical Commission,IEC)制定的一系列编码方案(也称字符集)。ISO 8859包括了15个针对不同语系的字符集 ,从ISO 8859-1到ISO 8859-16(ISO 8859-12未定义)。ISO 8859中每一个字符集都包括了128个ASCII字符和额外的128个字符 ,我们常见的是ISO 8859-1 ,也被称为Latin-1 ,主要包括了西欧常用字符,还包括德法两国的字母。

5. SBCS,DBCS和MBCS

上文提到的ASCII,扩展ASCII和ISO8859都是在单字节上进行编码的,因此它们都属于单字节字符集(Single-Byte Character Set,SBCS)。由于单字节字符集最多只能有2^8=256个位置可以使用,对于亚洲国家的字符来说是不够用的,所以逐渐出现了双字节字符集(Double-Byte CharacterSet, DBCS)、多字节字符集(Multi-Byte Character Set, MBCS)等。

第二阶段:本土化--多字节字符集

上图列出的BIG5台湾地区繁体中文标准字符集Shift_JIS日文标准字符集 ,这两个字符集都用单字节编码ASCII,用双字节编码汉字和日文 。用Python看一下两种编码方式的结果(麟的繁体字是麟,日文百度了一下貌似是りん,我也不知道)

python
#1. 使用BIG5-繁体字+英文
char_c = '麟'.encode('BIG5')
char_e = 'l'.encode('BIG5')
#1.1 打印编码
print(char_c) # b'\xc5\xef'
print(char_e) # b'l'
#1.2 打印编码后的数据类型
type(char_c) # <class 'bytes'>
type(char_e) # <class 'bytes'>
#1.3 打印字节长度
len(char_c) # 2
len(char_e) # 1
#2.使用Shift_JIS-日文+英文
char_j = 'りん'.encode('Shift_JIS')
char_e1 = 'l'.encode('Shift_JIS')
#2.1 打印编码
print(char_j) # b'\x82\xe8\x82\xf1'
print(char_e1) # b'l'
#2.2 打印编码后的数据类型
type(char_j) # <class 'bytes'>
type(char_e1) # <class 'bytes'>
#2.3 打印字节长度
len(char_j) # 4
len(char_e1) # 1

从代码结果可以看出,BIG5和Shift_JIS对于英文字母“l”使用一个字节编码,对于汉字和日文使用两个字节编码(日文中“麟”有2个字符所以字节长度是4,单字符的话字节长度还是2)。

6. GB2312,GBK和GB18030

6.1 GB2312

接下来着重介绍中文字符集的发展。GB2312-80 是第一个汉字编码国家标准,全称信息交换用汉字编码字符集 基本集(Code of Chinese Graphic Character Set for Information Interchange--Primary Set),由国家标准总局于1980年3月9日发布,1981年5月1日实施。GB国家标准(Guojia Biaozhun)的缩写。GB2312-80也被称作GB2312 ,共收录7445个字符,其中汉字6763个,拉丁字母、希腊字母、日本片假名等字符共682个。汉字包括一级汉字3755个和二级汉字3008个。

GB2312使用双字节对汉字编码,单字节对英文字符编码 。GB2312将所收录的字符进行分区处理,设定了94个区,每个区94个位 来存储字符,共有94*94=8836个位置,但并不是所有的位置都被填满。每一个字符都由一个区码和一个位码共同表示,这种表示方式也被称为区位码 。放一张GB2312字符表的示意图:

GB2312区位码示意图

上图标红的数字就是区码,“横纵坐标”就是位码,例如第一个汉字“啊”的区位码就是1601。

GB2312使用两个字节对汉字进行编码,第一字节被称为区域字节或高字节,第二字节被称为位字节或低字节 。将每一个汉字的区码和位码分别加上0xA0就是这个汉字的GB2312编码(0x是十六进制标识符)。举例来说,“麟”在字符集中的区位码是8775 (查表得出),区码和位码转换成十六进制为0x57和0x4B,分别加上0xA0后结果为0xF70xEB ,最终GB2312的编码就是0xF7EB 。使用代码验证一下:

python
#使用GB2312对汉字编码
char_c = '麟'.encode('GB2312')
char_e = 'l'.encode('GB2312')
#1. 打印编码
print(char_c) # b'\xf7\xeb'
print(char_e) # b'l'
#2. 打印编码后数据类型
type(char_c) # <class 'bytes'>
type(char_e) # <class 'bytes'>
#3. 打印字节长度
len(char_c) # 2
len(char_e) # 1

从代码中可以看到,汉字字符被编码后的字节长度为2,英文字母“l”被编码后字节长度为1。另外,这里的运算顺序不重要,先用87和75分别加160(0xA0)再转十六进制也可以。

6.2 GBK

GB2312收录的汉字可以满足日常生活中的用语,但是对于古汉语、人名中的罕见字和生僻字,还有繁体字都无法处理。所以在1995年12月,全国信息技术标准化技术委员会制订了汉字内码扩展规范(Chinese Internal Code Specification),也被简称为GBK(Guojia Biaozhun Kuozhan)。GBK对汉字采用双字节编码,对英文采用单字节编码,兼容ASCII和GB2312 。GBK共收录21886个符号,其中汉字有21003个,GBK将中文简体、繁体和日文都进行了扩充。

6.3 GB18030

随着国内少数民族对于计算机使用的增加,少数民族的文字也需要进行编码,2000年3月17日国家质量技术监督局推出了信息技术 信息交换用汉字编码字符集 基本集的扩充(Information Technology--Chinese Ideograms Coded Character Set for Information Interchange--Extension for the Basic Set, GB18030-2000),收录27533个汉字。2005年11月8日又发布了信息技术 中文编码字符集(Information Technology -- Chinese Coded Character Set, GB18030-2005/GB18030)。现行的是2005年发布的版本 ,2000年的版本已废止。

GB18030-2005覆盖中文、日文假名、朝鲜文和国内少数民族的文字等,共收录70244个汉字。同时,GB18030采用单字节、双字节和四字节三种方式对字符进行编码 。GB系列的字符集都可以在国家标准全文公开系统上查询到: http://openstd.samr.gov.cn/bzgk/gb/, GB18030向下兼容GBK,GB2312和ASCII

由于向下兼容,GB系列编码在对日常文字编码时结果都是相同的,上文代码中“麟”GB2312的编码结果是0xF7EB ,在Python中可以看到对“麟”使用GBK和GB18030编码的结果也是0xF7EB:

python
#1. 使用GBK对汉字编码
char_1 = '麟'.encode('GBK')
print(char_1) # b'\xf7\xeb'
#2. 使用GB18030对汉字编码
char_2 = '麟'.encode('GB18030')
print(char_2) # b'\xf7\xeb'

7. 代码页

7.1 代码页

在这里还要提出一个新的概念:代码页(Code Page) 。刚刚我们提到了4种编码方式,除去这些还有很多编码方式。当编码方式(或字符集)过多时,通过数字来标识各个字符集是一种方便的区分方法。代码页就是这种反映了数字和字符集之间映射关系的表 ,我们可以把它想象成一个目录。目前IBM,Microsoft,SAP,Oracle等公司都有自己的代码页,不同字符集在不同公司的代码页中被分配的页码不同 ,例如GBK在在IBM代码页被分配的ID为T1HK1114,在Windows代码页中为936。 IBM代码页 _https://www.ibm.com/support/knowledgecenter/SSLTBW_2.3.0/com.ibm.zos.v2r3.e0zx100/e0z2o00_codepagesoutlines.htm_ Windows代码页 _https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers_

还有一个问题,在Windows代码页中,ID 936对应的名称其实是gb2312 ,但是我在网上查了一圈大家都默认936就是gbk。我只能合理猜测由于GBK兼容GB2312且包含更多字符,所以936对应的字符集写的是gb2312实际却是gbk 另外,因为Code Page的缩写是CP ,GBK也经常被称为CP936

Windows代码页中936=gb2312

7.2 ANSI

上图我们发现了另外两个单词:ANSIOEM 。其实这是ANSI Code PageOEM Code Page 的简写,这两个代码页是目前Windows系统使用的两种代码页。

先说ANSI,有没有感觉很熟悉在文章第2节,ASCII就是由ANSI(美国国家标准学会)制订的,ANSI是一个非营利性质的团体,负责制订各界的标准化文件,我们提到的字符集只是标准化文件中的一种。网上很多帖子都把ANSI当作一种字符编码的方式,导致我在最开始研究这些的时候经常搞错,其实网上说的ANSI应该叫做ANSI Code Page,官方称作Windows Code Page, 就是我们刚刚提到的Windows代码页。

而且,ANSI Code Page也属于误称,之前在Windows Code Page中使用最广泛的字符集叫做windows-1252 ,这个字符集是根据ANSI草案(ANSI Draft)拟定的,ANSI草案后来发展成为了ISO 8859-1(第4节)。由于windows-1252符合ANSI草案中的标准,所以很多人就用ANSI编码(ANSI Encoding)代指windows-1252,后来又将这个概念拓展了一下,认为Windows Code Page中所有的字符集都是符合ANSI标准的,所以就直接用ANSI Code Page代替了Windows Code Page。其实这是错误的,在Windows Code Page也有一些不符合ANSI标准的字符集 。但是由于这种叫法太过普遍,后来微软官方也就承认了ANSI Code Page=Windows Code Page 的说法。

Windows代码页中1252=windows-1252

7.3 OEM

刚刚提到的ANSI Code Page基于图形用户界面,OEM Code Page应用于字符模式,也就是命令行界面 。最初设计OEM Code Page是为了向后兼容 ,也就是为了用户在使用Windows新的图形用户界面系统时也可以使用旧的命令行程序系统。

7.4 本地编码

还有一点,很多帖子中不仅会把ANSI当作一种编码方式,还会将它称为本地编码 。这种说法并不错误, 但其实本地编码的概念和Windows Code Page有关,不是ANSI。

Windows Code Page也被称为激活代码页(Active Code Page)或系统激活代码页(System Active Code Page) 。在一个Windows操作系统中,有且只有一个被激活的代码页(Active Windows Code Page)存在。简单来说就是一个操作系统当前只能使用一种编码方式 。这也很好理解,使用GBK字符集的时候无法打印出其他字符集中的字符,因为系统当前只激活了GBK的编码方式。

每一个Windows系统中都有语言设置,被激活的字符集和操作系统的语言总是保持一致,所以会被称作本地编码 。每一种语言设置都有一个默认使用的字符集,例如简体中文操作系统的默认字符集为GBK,繁体中文系统为BIG5,日文系统为Shift_JIS等。所以说,本地编码指的是Windows当前被激活的代码页所使用的字符集 (如果不做任何改动,就使用默认字符集)。被激活的代码页Active Code Page又被称为Windows Code Page,而Windows Code Page又被误称为ANSI Code Page或ANSI,所以ANSI就被认为是本地编码了

目前在Windows系统中对文件进行编码时,会出现“ANSI”选项,这里ANSI就代表了系统当前语言的默认编码方式,在11.3节中会有图示。有关代码页的内容在微软官网上有相关说明,网址是 _https://docs.microsoft.com/en-us/windows/win32/intl/code-pages?redirectedfrom=MSDN_

这部分的最后,贴一个Windows系统中查询默认编码方式的方法,非常简单,在命令提示符界面输入chcp 。我的系统是简体中文系统,下图可以看到代码页是936,也就是前文说的GBK(CP936)。

WIndows系统查询代码页

以上就是字符编码的第一阶段和第二阶段,下一篇开始进入全球化阶段~

~END~